package game;

import cards.Card;
import static cards.Card.Suit;
import static cards.Card.Face;

import java.util.Comparator;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Iterator;

public enum PokerHandRank implements HandRank {
  NOTHING {
    public boolean hasRank(Set<Card> cards) {
      return true;
    }
  },
  PAIR {
    public boolean hasRank(Set<Card> cards) {
      return containsValue(cards, 2) != null;
    }
  },
  TWO_PAIR {
    public boolean hasRank(Set<Card> cards) {
      Set<Card> firstPair = containsValue(cards, 2);
      if (firstPair != null) {
        Set<Card> remainingCards = new HashSet<Card>(cards);
        remainingCards.removeAll(firstPair);
        return containsValue(remainingCards, 2) != null;
      }
      return false;
    }
  },
  THREE_OF_A_KIND {
    public boolean hasRank(Set<Card> cards) {
      return containsValue(cards, 3) != null;
    }
  },
  STRAIGHT {
    public boolean hasRank(Set<Card> cards) {
      return containsStraight(cards) != null;
    }
  },
  FLUSH {
    public boolean hasRank(Set<Card> cards) {
      return containsFlush(cards) != null;
    }
  },
  FULL_HOUSE {
    public boolean hasRank(Set<Card> cards) {
      Set<Card> firstPair = containsValue(cards, 2);
      if (firstPair != null) {
        Set<Card> remainingCards = new HashSet<Card>(cards);
        remainingCards.removeAll(firstPair);
        return containsValue(remainingCards, 3) != null;
      }
      return false;
    }
  },
  FOUR_OF_A_KIND {
    public boolean hasRank(Set<Card> cards) {
      return containsValue(cards, 4) != null;
    }
  },
  STRAIGHT_FLUSH {
    public boolean hasRank(Set<Card> cards) {
      cards = containsFlush(cards);
      if (cards != null) {
        return containsStraight(cards) != null;
      }
      return false;
    }
  };

  public List<Card> selectBestMatch(Set<Card> cards) {
    // TODO
    return null;
  }

  private static Set<Card> containsValue(Set<Card> cards, int expectedCount) {
    EnumMap<Face, Set<Card>> values = new EnumMap<Face, Set<Card>>(Face.class);
    for (Card card : cards) {
      Face face = card.getFace();
      if (!values.containsKey(face)) {
        values.put(face, new HashSet<Card>());
      }
      values.get(face).add(card);
    }

    // Look for the closest match, meaning the smallest set that meets the
    // expectedValue length. If we have an exact match, return immediately
    // (and no, I don't care about branch prediciton).
    Set<Card> bestMatch = null;
    for (Map.Entry<Face, Set<Card>> entry : values.entrySet()) {
      if (entry.getValue().size() == expectedCount) {
        return entry.getValue();
      } else if (entry.getValue().size() > expectedCount
          && (bestMatch == null
            || entry.getValue().size() < bestMatch.size())) {
        bestMatch = entry.getValue();
      }
    }

    // if we found an imperfect match, trim it down to the expected size
    if (bestMatch != null) {
      assert bestMatch.size() > expectedCount;
      while (bestMatch.size() > expectedCount) {
        Iterator<Card> it = bestMatch.iterator();
        it.next();
        it.remove();
      }
      assert bestMatch.size() == expectedCount;
    }

    return bestMatch;
  }

  private static Set<Card> containsStraight(Set<Card> cards) {
    EnumMap<Face, Card> uniqueFaceCards = new EnumMap<Face, Card>(Face.class);
    for (Card card : cards) {
      uniqueFaceCards.put(card.getFace(), card);
    }
    cards = new TreeSet<Card>(uniqueFaceCards.values());

    Set<Card> currSequence = new HashSet<Card>();
    Set<Card> maxSequence = currSequence;
    Card lastCard = null;
    for (Card card : cards) {
      if (lastCard == null ||
          (card.getFace().ordinal() - lastCard.getFace().ordinal()) == 1) {
        currSequence.add(card);
        if (currSequence.size() > maxSequence.size()) {
          maxSequence = currSequence;
        }
      } else {
        currSequence = new HashSet<Card>();
      }
      lastCard = card;
    }
    // ACE can also be above KING
    if (lastCard != null && lastCard.getFace() == Face.KING
        && uniqueFaceCards.containsKey(Face.ACE)) {
      currSequence.add(uniqueFaceCards.get(Face.ACE));
      if (currSequence.size() > maxSequence.size()) {
        maxSequence = currSequence;
      }
    }
    if (maxSequence.size() > 4) {
      return maxSequence;
    }
    return null;
  }

  private static Set<Card> containsFlush(Set<Card> cards) {
    EnumMap<Suit, Set<Card>> suits = new EnumMap<Suit, Set<Card>>(Suit.class);
    for (Suit suit : Suit.values()) {
      suits.put(suit, new HashSet<Card>());
    }
    for (Card card : cards) {
      suits.get(card.getSuit()).add(card);
    }
    for (Suit suit : Suit.values()) {
      if (suits.get(suit).size() > 4) {
        return suits.get(suit);
      }
    }
    return null;
  }

  public static class PokerHandRankComparator
      implements Comparator<HandRank> {
    public int compare(HandRank r1, HandRank r2) {
      PokerHandRank pr1 = (PokerHandRank)r1;
      PokerHandRank pr2 = (PokerHandRank)r2;
      // TODO: Tie breaker when pr1 == pr2
      return pr1.compareTo(pr2);
    }
  }

  public static class PlayerHandRankComparator implements Comparator<PokerPlayer> {
    private static final Comparator<HandRank> comparator =
        new PokerHandRankComparator();
    public int compare(PokerPlayer p1, PokerPlayer p2) {
      return comparator.compare(p1.privateHandRank(), p2.privateHandRank());
    }
  }
}
